package viewer;

import javax.swing.event.*;

import model.RoffLine2D;
import model.RoffVector3D;

public class RoffPerspective {

    private RoffVector3D fromPoint;
    private RoffVector3D atPoint;
    private RoffVector3D upVector;

    private double viewAngle;
    private double tanHalfAngle;

    private int screenScaleX;
    private int screenScaleY;
    private double screenScaleXHalf;
    private double screenScaleYHalf;
    private double aspectRatio;

    private double factor2DX;
    private double factor2DY;

    // Transformation matrix V
    private RoffVector3D aRow;
    private RoffVector3D bRow;
    private RoffVector3D cRow;

    private ChangeListener cl;

    public RoffPerspective() {

        upVector = new RoffVector3D(0, 1, 0);
        atPoint = new RoffVector3D(0, 0, 0);
        fromPoint = new RoffVector3D(1, 1, 1);

        screenScaleX = 1;
        screenScaleY = 1;
        screenScaleXHalf = (double)screenScaleX / 2.0;
        screenScaleYHalf = (double)screenScaleY / 2.0;
        aspectRatio = 1.0;
 
        viewAngle = 45;
        tanHalfAngle = Math.tan(viewAngle * Math.PI / 360.0);

        updateFactors();

        cl = null;
        updateTransformation();
    }

    public void setChangeListener(ChangeListener cl) {
        this.cl = cl;
    }

    public void setFrom(RoffVector3D newFrom) {
        fromPoint.x = newFrom.x;
        fromPoint.y = newFrom.y;
        fromPoint.z = newFrom.z;
        updateTransformation();
    }

    public void setAt(RoffVector3D newAt) {
        atPoint.x = newAt.x;
        atPoint.y = newAt.y;
        atPoint.z = newAt.z;
        updateTransformation();
    }

    public void setUp(RoffVector3D newUp) {
        upVector = newUp;
        updateTransformation();
    }

    public RoffVector3D getFrom() {
        return new RoffVector3D(fromPoint.x, fromPoint.y, fromPoint.z);
    }

    public RoffVector3D getAt() {
        return new RoffVector3D(atPoint.x, atPoint.y, atPoint.z);
    }

    public RoffVector3D getUp() {
        return new RoffVector3D(upVector.x, upVector.y, upVector.z);
    }

    public double getAngle() {
        return viewAngle;
    }

    public double getAspectRatio() {
        return aspectRatio;
    }

    public void setScreenScale(int x, int y) {
        screenScaleX = x;
        screenScaleY = y;
        screenScaleXHalf = (double)screenScaleX / 2.0;
        screenScaleYHalf = (double)screenScaleY / 2.0;
	aspectRatio = (double)x / (double)y;
        updateFactors();
        if (cl != null) {
            cl.stateChanged(new ChangeEvent(this));
        }
    }

    public void setAngle(double newAngle) {
        viewAngle = newAngle;
        if (viewAngle > 179) {
            viewAngle = 179.0;
	} else if (viewAngle < .5) {
            viewAngle = .5;
        }
        tanHalfAngle = Math.tan(viewAngle * Math.PI / 360.0);
        updateFactors();
        if (cl != null) {
            cl.stateChanged(new ChangeEvent(this));
        }
    }

    public void zoom(double percentage) {
        double scaleFactor = percentage / 100.0;
        boolean validScale = false;
        RoffVector3D lineOfSight = atPoint.sub(fromPoint);

        if (percentage < 100) {
            if (lineOfSight.getLength() > .1) {
                validScale = true;
            }
        } else {
            if (lineOfSight.getLength() < 10000) {
                validScale = true;
            }
        }
        if (validScale) {
            // Zoom - ((F - A) * scale) + A
            fromPoint.decBy(atPoint);
            fromPoint.scalarMultiplyBy(scaleFactor);
            fromPoint.incBy(atPoint);
        }

        updateTransformation();
    }

    public void translate(double rightPercent, double upPercent) {
        RoffVector3D vectorLoS = atPoint.sub(fromPoint);

        RoffVector3D horizon = upVector.cross(vectorLoS);
        RoffVector3D centerLine = vectorLoS.cross(horizon);

        RoffVector3D offsets = (horizon.scalarMultiply(rightPercent)).add(centerLine.scalarMultiply(upPercent));

        atPoint.incBy(offsets);
        fromPoint.incBy(offsets);

        updateTransformation();
    }

/*
    // Alternate implementation of the rotate function. Left in, commented out, simply because
    // it's nicely commented and more interesting than the other implementation.

    public void rotate(double upDegrees, double rightDegrees) {

         // Note that "up" and "down" are reversed from what you
        // might think, because we are moving in eye coordinates,
        // which are left-handed.

        // Move the up vector to match.
        // This must be assigned before we change fromPoint,
        // because WorldToEye uses fromPoint.
        upVector.incBy(fromPoint);
        upVector = WorldToEye(upVector);
        upVector.rotateX(-upDegrees);
        upVector.rotateY(-upDegrees);
        upVector = EyeToWorld(upVector);

	// Move the from point
	// Find the at point in eye coordinates. This will lie on the z-axis,
	// by construction. The from point will be at the origin of eye coordinates.
	RoffVector eyeAt = WorldToEye(atPoint);
	// We want to rotate the from point around the at point, so we must translate
	// along z to find the from we will rotate, with the at at the origin. This
	// is equivalent to saying WorldToEye(fromPoint) - WorldToEye(atPoint),
	// because WorldToEye(fromPoint) is 0,0,0.
	RoffVector movingFrom = eyeAt.scalarMultiply(-1.0);
	// Eye coordinates are left-handed, X goes to the right of the screen.
	// Therefore, going around X in the positive direction yields counterclockwise
	// rotation around the axis, and moves us "down" in eye coordinates.
	movingFrom.rotateX(-upDegrees);
	// Eye coordinates are left-handed, Y goes to the top of the screen.
	// Therefore, going around Y in the positive direction yields counterclockwise
	// rotation around the axis, and moves us "right" in eye coordinates.
  	movingFrom.rotateY(rightDegrees);
        // Now we reverse the construction. We add back the eye version of the
	// at point, moving forward in eye-Z again. We then move back into
	// world coordinates.
        movingFrom.incBy(eyeAt);
        RoffVector oldFrom = fromPoint;
	fromPoint = EyeToWorld(movingFrom);
        fromPoint.incBy(oldFrom);

	updateTransformation();
    }
*/

    public void rotate(double upDegrees, double rightDegrees) {

        fromPoint.decBy(atPoint);
        RoffVector3D horizon = upVector.cross(fromPoint);
        RoffVector3D centerLine = horizon.cross(fromPoint);

        if (upDegrees != 0) {
            fromPoint = fromPoint.rotate(horizon, upDegrees);
            upVector = upVector.rotate(horizon, upDegrees);
        }

        if (rightDegrees != 0) {
            fromPoint = fromPoint.rotate(centerLine, rightDegrees);
            upVector = upVector.rotate(centerLine, rightDegrees);
        }

        fromPoint.incBy(atPoint);

        updateTransformation();
    }

    public RoffVector3D WorldToEye(RoffVector3D worldPoint) {
        // World point - F
        RoffVector3D factors = worldPoint.sub(fromPoint);
	
        return new RoffVector3D(factors.dot(aRow),
                              factors.dot(bRow),
                              factors.dot(cRow));
    }

    public RoffVector3D EyeToWorld(RoffVector3D eyePoint) {
	// eyePoint * V^T
	return new RoffVector3D((eyePoint.x * aRow.x +
                               eyePoint.y * bRow.x +
                               eyePoint.z * cRow.x),
                              (eyePoint.x * aRow.y +
                               eyePoint.y * bRow.y +
                               eyePoint.z * cRow.y),
                              (eyePoint.x * aRow.z +
		               eyePoint.y * bRow.z +
                               eyePoint.z * cRow.z));
    }

    public RoffVector3D EyeTo2D(RoffVector3D toConvert) {
        return new RoffVector3D((factor2DX * toConvert.x / toConvert.z) + screenScaleXHalf, 
		              (factor2DY * toConvert.y / toConvert.z) + screenScaleYHalf,
			      toConvert.z);
    }

    public RoffVector3D WorldTo2D(RoffVector3D worldPoint) {
        // World point - F
        RoffVector3D factors = worldPoint.sub(fromPoint);
	
        double z = factors.dot(cRow);
        if (z == 0.0) {
            System.err.println("Divide by zero avoided in WorldTo2D()");
            z = Float.MIN_VALUE;
        }
        return new RoffVector3D((factor2DX * factors.dot(aRow) / z) + screenScaleXHalf, 
		              (factor2DY * factors.dot(bRow) / z) + screenScaleYHalf,
			      z);
    }

    public RoffVector3D EyeToClip(RoffVector3D toConvert) {
        return new RoffVector3D(toConvert.x / (tanHalfAngle * aspectRatio),
                              toConvert.y / tanHalfAngle,
                              toConvert.z);
    }

    public RoffVector3D WorldToClip(RoffVector3D worldPoint) {
        RoffVector3D factors = worldPoint.sub(fromPoint);
	
        return new RoffVector3D(factors.dot(aRow) / (tanHalfAngle * aspectRatio),
                              factors.dot(bRow) / tanHalfAngle,
                              factors.dot(cRow));
    }

    public RoffVector3D ClipTo2D(RoffVector3D toConvert) {
        return new RoffVector3D((screenScaleXHalf * toConvert.x / toConvert.z) + screenScaleXHalf,
                              (-screenScaleYHalf * toConvert.y / toConvert.z) + screenScaleYHalf,
                              toConvert.z);
    }

    public void clipToLine(RoffVector3D startPoint, RoffVector3D endPoint, RoffLine2D output) {

        RoffVector3D start = WorldToClip(startPoint);
        RoffVector3D end = WorldToClip(endPoint);

        boolean startAbove = (start.y > start.z);
        boolean startLeft = (start.x < -start.z);
        boolean startRight = (start.x > start.z);
        boolean startBelow = (start.y < -start.z);
        boolean endAbove = (end.y > end.z);
        boolean endLeft = (end.x < -end.z);
        boolean endRight = (end.x > end.z);
        boolean endBelow = (end.y < -end.z);

        if ((startLeft & endLeft) ||
            (startAbove & endAbove) ||
            (startRight & endRight) ||
            (startBelow & endBelow)
           ) {
            // Trivially reject
            output.valid = false;
            return;
        }
        RoffVector3D lineDir = null;
        double t = 0;

        // If the line crosses the top plane
        if (startAbove ^ endAbove) {
            if (startAbove) {
                // Move start down
                // P0 = end;
                // P1 = start;
                lineDir = start.sub(end);
                t = -(end.y - end.z) / (lineDir.y - lineDir.z);
                t = t * .999;
                start = end.add(lineDir.scalarMultiply(t));
                startLeft = (start.x < -start.z);
                startRight = (start.x > start.z);
                startBelow = (start.y > start.z);
            } else {
                // Move end down
                lineDir = end.sub(start);
                t = -(start.y - start.z) / (lineDir.y - lineDir.z);
                t = t * .999;
                end = start.add(lineDir.scalarMultiply(t));
                endLeft = (end.x < -end.z);
                endRight = (end.x > end.z);
                endBelow = (end.y > end.z);
            }
            if ((startLeft & endLeft) ||
                (startRight & endRight) ||
                (startBelow & endBelow)
                ) {
                // Trivially reject
                output.valid = false;
                return;
            }
        }

        // If the line crosses the left plane
        if (startLeft ^ endLeft) {
            if (startLeft) {
                // Move start right
                // P0 = end;
                // P1 = start;
                lineDir = start.sub(end);
                t = -(end.x + end.z) / (lineDir.x + lineDir.z);
                t = t * .999;
                start = end.add(lineDir.scalarMultiply(t));
                startRight = (start.x > start.z);
                startBelow = (start.y > start.z);
            } else {
                // Move end right
                lineDir = end.sub(start);
                t = -(start.x + start.z) / (lineDir.x + lineDir.z);
                t = t * .999;
                end = start.add(lineDir.scalarMultiply(t));
                endRight = (end.x > end.z);
                endBelow = (end.y > end.z);
            }
            if ((startRight & endRight) ||
                (startBelow & endBelow)
                ) {
                // Trivially reject
                output.valid = false;
                return;
            }
        }

        // If the line crosses the right plane
        if (startRight ^ endRight) {
            if (startRight) {
                // Move start left
                // P0 = end;
                // P1 = start;
                lineDir = start.sub(end);
                t = -(end.x - end.z) / (lineDir.x - lineDir.z);
                t = t * .999;
                start = end.add(lineDir.scalarMultiply(t));
                startBelow = (start.y > start.z);
            } else {
                // Move end left
                lineDir = end.sub(start);
                t = -(start.x - start.z) / (lineDir.x - lineDir.z);
                t = t * .999;
                end = start.add(lineDir.scalarMultiply(t));
                endBelow = (end.y > end.z);
            }
            if (startBelow & endBelow) {
                // Trivially reject
                output.valid = false;
                return;
            }
        }

        // If the line crosses the bottom plane
        if (startBelow ^ endBelow) {
            if (startBelow) {
                // Move start up
                // P0 = end;
                // P1 = start;
                lineDir = start.sub(end);
                t = -(end.y + end.z) / (lineDir.y + lineDir.z);
                t = t * .999;
                start = end.add(lineDir.scalarMultiply(t));
            } else {
                // Move end up
                lineDir = end.sub(start);
                t = -(start.y + start.z) / (lineDir.y + lineDir.z);
                t = t * .999;
                end = start.add(lineDir.scalarMultiply(t));
            }
        }

        // If we got this far, we must have something left to output,
        // so do so.
        RoffVector3D temp = ClipTo2D(start);
        output.startX = (int) temp.x;
        output.startY = (int) temp.y;
        temp = ClipTo2D(end);
        output.endX = (int) temp.x;
        output.endY = (int) temp.y;
        output.valid = true;
    }

    private void updateTransformation() {
        cRow = atPoint.sub(fromPoint);
        aRow = cRow.cross(upVector);
        bRow = aRow.cross(cRow);
        aRow.normalize();
        bRow.normalize();
        cRow.normalize();
        if (cl != null) {
            cl.stateChanged(new ChangeEvent(this));
        }
    }

    private void updateFactors() {
        factor2DX = (double)screenScaleX/(2.0 * aspectRatio * tanHalfAngle);
        factor2DY = (double)-screenScaleY/(2.0 * tanHalfAngle);
    }
}